1 module jupyter.wire.message;
2 
3 
4 import std.json: JSONValue;
5 
6 /**
7    A message sent to the kernel.
8    See https://jupyter-client.readthedocs.io/en/stable/messaging.html#wire-protocol
9  */
10 struct Message {
11     import std.json: JSONValue;
12 
13     string[] identities;
14     MessageHeader header;
15     MessageHeader parentHeader;
16     // Not using asdf here because it's a pain to construct JSON objects with it
17     // and its serialisation doesn't buy anything since both metadata and content
18     // are free-form.
19     JSONValue metadata;
20     JSONValue content;
21     string[] extraRawData;
22 
23     enum delimiter = "<IDS|MSG>";
24 
25     /**
26        Constructs the message from the strings sent to the control or shell
27        sockets.
28      */
29     this(in string[] strings) @safe pure {
30         import asdf: deserialize;
31         import std.json: parseJSON;
32         import std.algorithm: countUntil;
33 
34         const delimiterIndex = strings.countUntil(delimiter);
35         identities = strings[0 .. delimiterIndex].dup;
36 
37         // TODO: verify signature
38         // hmac is delimiter + 1
39 
40         () @trusted {
41             header = strings[delimiterIndex + 2].deserialize!MessageHeader;
42             parentHeader = strings[delimiterIndex + 3].deserialize!MessageHeader;
43         }();
44 
45         metadata = parseJSON(strings[delimiterIndex + 4]);
46         content = parseJSON(strings[delimiterIndex + 5]);
47         extraRawData = strings[delimiterIndex + 6 .. $].dup;
48     }
49 
50     this(in Message other, in string msgType, JSONValue content) @safe {
51         identities = other.identities.dup;
52         this(other.header, msgType, content);
53     }
54 
55     this(in MessageHeader parentHeader, in string msgType) @safe {
56         import std.json: parseJSON;
57         this(parentHeader, msgType, parseJSON(`{}`));
58     }
59 
60     this(in MessageHeader parentHeader, in string msgType, JSONValue content) @safe {
61         this.header = this.parentHeader = parentHeader;
62         this.header.msgType = msgType;
63         updateHeader;
64         this.content = content;
65     }
66 
67     /**
68        Convert to a format suitable for sending over ZMQ
69      */
70     string[] toStrings(in string key) @safe const {
71         return
72             identities.dup ~
73             delimiter ~
74             signature(key) ~
75             header.toJsonString ~
76             parentHeader.toJsonString ~
77             metadata.toJsonString ~
78             content.toJsonString ~
79             extraRawData;
80     }
81 
82     /**
83        Update header with a random uuid and setting the timestamp
84      */
85     void updateHeader() @safe {
86         import std.datetime: DateTime, Clock;
87         import std.uuid: randomUUID;
88 
89         header.date = (cast(DateTime)Clock.currTime).toISOExtString;
90         header.msgID = randomUUID.toString;
91     }
92 
93     private string signature(in string key) @safe const {
94         import std.digest.hmac: hmac;
95         import std.digest.sha: SHA256;
96         import std.string: representation;
97         import std.array : appender;
98         import std.conv : toChars;
99 
100         auto mac = hmac!SHA256(key.representation);
101 
102         foreach(w; [header.toJsonString, parentHeader.toJsonString, toJsonString(metadata), toJsonString(content)])
103             mac.put(w.representation);
104 
105         ubyte[32] us = mac.finish;
106         auto cs = appender!string;
107         cs.reserve(64);
108 
109         foreach(u; us[]) {
110             if (u <= 0xf) cs.put('0');
111             cs.put(toChars!16(cast(uint) u));
112         }
113 
114         return cs.data;
115     }
116 }
117 
118 
119 struct MessageHeader {
120     import mir.serde: serdeKeys, serdeOptional;
121 
122     @serdeOptional
123     @serdeKeys("msg_id")   string msgID;
124     @serdeOptional
125     @serdeKeys("msg_type") string msgType;
126     @serdeOptional
127     @serdeKeys("username") string userName;
128     @serdeOptional         string session;
129     @serdeOptional         string date;
130     @serdeOptional
131     @serdeKeys("version")  string protocolVersion;
132 
133     static empty() {
134         import jupyter.wire.kernel : protocolVersion;
135         auto header = MessageHeader();
136         header.protocolVersion = protocolVersion;
137         return header;
138     }
139 }
140 
141 // can't be made a member function because `serializetoJson(this)` doesn't compile
142 private string toJsonString(MessageHeader header) @safe pure {
143     import asdf: serializeToJson;
144     import std.string: replace;
145     const prelim = header.msgID is null ? "{}" : () @trusted { return serializeToJson(header); }();
146     return prelim.replace("null", `""`);
147 }
148 
149 // can't be pure due to JSONValue.toString
150 private string toJsonString(in JSONValue json) @safe {
151     import std.json: JSONType;
152     return json.type == JSONType.null_ ? `{}` : json.toString;
153 }
154 
155 
156 Message statusMessage(MessageHeader header, in string status) @safe {
157     import std.json: JSONValue;
158     JSONValue content;
159     content["execution_state"] = status;
160     auto ret = pubMessage(header, "status", content);
161     return ret;
162 }
163 
164 
165 Message pubMessage(MessageHeader header, in string msgType, JSONValue content, JSONValue metadata = JSONValue()) @safe {
166     import std.json : JSONType, parseJSON;
167     auto ret = Message(header, msgType, content);
168     ret.identities = [msgType];
169     if (metadata.type == JSONType.object)
170         ret.metadata = metadata;
171     else
172         ret.metadata = parseJSON(`{}`);
173     return ret;
174 }
175 
176 
177 struct CompleteResult {
178     string[] matches;
179     long cursorStart;
180     long cursorEnd;
181     string[string] metadata;
182     string status;
183 }
184 
185 
186 Message completeMessage(in Message requestMessage, in CompleteResult result) @safe {
187     import std.json: JSONValue;
188 
189     JSONValue content;
190     content["matches"] = result.matches;
191     content["cursor_start"] = result.cursorStart;
192     content["cursor_end"] = result.cursorEnd;
193     content["metadata"] = result.metadata;
194     content["status"] = result.status;
195 
196     return Message(requestMessage, "complete_reply", content);
197 }
198 
199 Message commOpenMessage(in string commId, in string targetName, JSONValue data = JSONValue(), JSONValue metadata = JSONValue()) @safe {
200 
201     JSONValue content;
202     content["comm_id"] = commId;
203     content["target_name"] = targetName;
204     content["data"] = data;
205 
206     return pubMessage(MessageHeader.empty(), "comm_open", content, metadata);
207 }
208 
209 Message commCloseMessage(in Message requestMessage) @safe {
210 
211     JSONValue content;
212     content["comm_id"] = requestMessage.content["comm_id"];
213 
214     return pubMessage(requestMessage.header, "comm_close", content);
215 }
216 
217 Message commCloseMessage(in string commId, JSONValue data = JSONValue()) @safe {
218 
219     JSONValue content;
220     content["comm_id"] = commId;
221     content["data"] = data;
222 
223     return pubMessage(MessageHeader.empty(), "comm_close", content);
224 }
225 
226 Message commMessage(in string commId, JSONValue data = JSONValue(), JSONValue metadata = JSONValue()) @safe {
227 
228     JSONValue content;
229     content["comm_id"] = commId;
230     content["data"] = data;
231 
232     return pubMessage(MessageHeader.empty(), "comm_msg", content, metadata);
233 }
234 
235 Message displayDataMessage(JSONValue data, JSONValue metadata = JSONValue()) @safe {
236 
237     import std.json : JSONType, parseJSON;
238     JSONValue content;
239     content["data"] = data;
240     content["metadata"] = metadata.type == JSONType.object ? metadata : parseJSON(`{}`);
241 
242     return pubMessage(MessageHeader.empty(), "display_data", content);
243 }